Skip to content

perf(investments): cut InvestmentsPage DB fanout and add CI#2

Merged
felamaslen merged 10 commits into
mainfrom
perf/investments-n-1
Apr 19, 2026
Merged

perf(investments): cut InvestmentsPage DB fanout and add CI#2
felamaslen merged 10 commits into
mainfrom
perf/investments-n-1

Conversation

@felamaslen

@felamaslen felamaslen commented Apr 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Collapses the InvestmentsPage query's DB fanout from ~230 drizzle spans → ~17, and cumulative DB time from ~8.9 s → ~100 ms, by batching loadInvestmentStats with dataloader and memoising loadHeldInvestments per Portfolio instance.
  • Dockerises the backend for dev and prod (packages/backend/Dockerfile, compose override), adds a root .dockerignore, and pins Node 24 via .tool-versions.
  • Adds .github/workflows/ci.yml — a single test job that runs pnpm typecheck, pnpm exec eslint ., then pnpm --filter backend test against a Postgres 18 service.

Perf — InvestmentsPage (sampled via Jaeger, 3 traces each)

Stage Drizzle spans Cum DB Wall
Before 229 ~8.9 s ~290 ms
Per-key memo of loadInvestmentStats 137 ~2.4 s ~290 ms
dataloader batch (one IN (...) per table) 29 ~450 ms ~150 ms
Per-Portfolio memo of loadHeldInvestments 17 ~100 ms ~100–180 ms

Infra

  • Dockerfile is prod-oriented (default CMD runs pnpm start); compose overrides to pnpm dev and bind-mounts the host source tree so vite-node hot-reloads without rebuilds. OTEL_* env points the container at the in-network Jaeger so traces show up in the existing jaegertracing/all-in-one service.
  • README updated with dev setup, mermaid architecture diagram, OTel docs, feature set.

Test plan

  • CI green (typecheck + lint + backend tests).
  • InvestmentsPage renders identical totals / gains in the browser.
  • docker compose up -d brings backend, Postgres, and Jaeger up cleanly; traces appear at http://localhost:16686 under fire-backend.

Adds a `node:24-alpine` Dockerfile that bakes in deps and sources and
defaults to `pnpm start` (prod). The compose file overrides `command`
to `pnpm dev` and volume-mounts the host source tree for hot reload,
and points the container at the in-network Jaeger for OTLP traces.

Also pins Node to 24 via `.tool-versions` + `engines` so host and
container match.
Wraps `loadInvestmentStats` in `contextAwareDataLoader` keyed on
`(investmentId, assetId)`, and threads `Context` through the four
callers that hit it. Cuts the per-investment query fanout in half
(46 → 23 per `InvestmentsPage`), since the `investments()` sort and
`Investment.position` resolver no longer re-run the four underlying
selects for the same investment.
Replaces the per-id memo with a microtask-coalesced batcher: every
`loadInvestmentStats(ctx, id)` call issued within the same tick is
folded into four `IN (...)` selects — one each for `Investments`,
`InvestmentTransactions`, `InvestmentStockSplits`, and
`InvestmentPrices`. Stats are then computed from the grouped rows.

On `InvestmentsPage`, the four per-id queries that used to fire 23×
each collapse into a single batched round-trip each, dropping the
total drizzle-span count from ~230 to ~50 and halving cumulative DB
time. `assetId` filtering runs in JS over the batched transactions
so per-wrapper resolvers share the same fetch.
Every Portfolio field resolver (totalValue, totalCost, totalGain,
percentGain, xirr, dailyGainValue, dailyGainPercent, timeseries,
candlestick) used to call `loadHeldInvestments` independently, firing
the same four `IN (...)` selects once per field. `Portfolio` now has
a private `loadHeld(opts)` method that caches by normalised
`skipLive` flag, and `computePortfolioXirr` / `loadDailySeriesMinor`
accept the already-loaded held list as a parameter so they share the
cache.

On the `InvestmentsPage` query this drops drizzle spans from ~29 to
~17 and cumulative DB time from ~450 ms to ~100 ms.
Two parallel jobs:
- `lint`: runs `pnpm typecheck` and `pnpm exec eslint .` at the repo root.
- `backend-test`: spins up Postgres 18 on port 5433 (matching
  `test/global-setup.ts`) and runs `pnpm --filter backend test`.
- `pnpm exec eslint . --fix` to apply prettier formatting across the tree.
- Allow `process` / `console` as globals in `**/*.mjs` (covers
  `packages/backend/otel.mjs`, which runs as a plain Node script).
- Drop the unused `FormSection` helper and its orphaned imports from
  `packages/web/src/components/net-worth/entry-form.tsx`.
- Rename unused `year` destructures to `_year` in
  `packages/web/src/routes/planning/$year.tsx`.

`pnpm exec eslint .` is now clean.
@felamaslen felamaslen merged commit 7cf9838 into main Apr 19, 2026
1 check passed
felamaslen added a commit that referenced this pull request Apr 27, 2026
perf(investments): cut InvestmentsPage DB fanout and add CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant